/**
 * @file at.c
 * Generic AT command handling library implementation
 */

/*
 * Copyright (c) 2015-2016 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <ctype.h>
#include <errno.h>
#include <net/buf.h>
#include <stdarg.h>
#include <string.h>

#include "at.h"

static void next_list(struct at_client *at) {
  if (at->buf[at->pos] == ',') {
    at->pos++;
  }
}

int at_check_byte(struct net_buf *buf, char check_byte) {
  const unsigned char *str = buf->data;

  if (*str != check_byte) {
    return -EINVAL;
  }
  net_buf_pull(buf, 1);

  return 0;
}

static void skip_space(struct at_client *at) {
  while (at->buf[at->pos] == ' ') {
    at->pos++;
  }
}

int at_get_number(struct at_client *at, uint32_t *val) {
  uint32_t i;

  skip_space(at);

  for (i = 0U, *val = 0U; isdigit((unsigned char)at->buf[at->pos]); at->pos++, i++) {
    *val = *val * 10U + at->buf[at->pos] - '0';
  }

  if (i == 0U) {
    return -ENODATA;
  }

  next_list(at);
  return 0;
}

static bool str_has_prefix(const char *str, const char *prefix) {
  if (strncmp(str, prefix, strlen(prefix)) != 0) {
    return false;
  }

  return true;
}

static int at_parse_result(const char *str, struct net_buf *buf, enum at_result *result) {
  /* Map the result and check for end lf */
  if ((!strncmp(str, "OK", 2)) && (at_check_byte(buf, '\n') == 0)) {
    *result = AT_RESULT_OK;
    return 0;
  }

  if ((!strncmp(str, "ERROR", 5)) && (at_check_byte(buf, '\n')) == 0) {
    *result = AT_RESULT_ERROR;
    return 0;
  }

  return -ENOMSG;
}

static int get_cmd_value(struct at_client *at, struct net_buf *buf, char stop_byte, enum at_cmd_state cmd_state) {
  int         cmd_len = 0;
  uint8_t     pos     = at->pos;
  const char *str     = (char *)buf->data;

  while (cmd_len < buf->len && at->pos != at->buf_max_len) {
    if (*str != stop_byte) {
      at->buf[at->pos++] = *str;
      cmd_len++;
      str++;
      pos = at->pos;
    } else {
      cmd_len++;
      at->buf[at->pos] = '\0';
      at->pos          = 0U;
      at->cmd_state    = cmd_state;
      break;
    }
  }
  net_buf_pull(buf, cmd_len);

  if (pos == at->buf_max_len) {
    return -ENOBUFS;
  }

  return 0;
}

static int get_response_string(struct at_client *at, struct net_buf *buf, char stop_byte, enum at_state state) {
  int         cmd_len = 0;
  uint8_t     pos     = at->pos;
  const char *str     = (char *)buf->data;

  while (cmd_len < buf->len && at->pos != at->buf_max_len) {
    if (*str != stop_byte) {
      at->buf[at->pos++] = *str;
      cmd_len++;
      str++;
      pos = at->pos;
    } else {
      cmd_len++;
      at->buf[at->pos] = '\0';
      at->pos          = 0U;
      at->state        = state;
      break;
    }
  }
  net_buf_pull(buf, cmd_len);

  if (pos == at->buf_max_len) {
    return -ENOBUFS;
  }

  return 0;
}

static void reset_buffer(struct at_client *at) {
  (void)memset(at->buf, 0, at->buf_max_len);
  at->pos = 0U;
}

static int at_state_start(struct at_client *at, struct net_buf *buf) {
  int err;

  err = at_check_byte(buf, '\r');
  if (err < 0) {
    return err;
  }
  at->state = AT_STATE_START_CR;

  return 0;
}

static int at_state_start_cr(struct at_client *at, struct net_buf *buf) {
  int err;

  err = at_check_byte(buf, '\n');
  if (err < 0) {
    return err;
  }
  at->state = AT_STATE_START_LF;

  return 0;
}

static int at_state_start_lf(struct at_client *at, struct net_buf *buf) {
  reset_buffer(at);
  if (at_check_byte(buf, '+') == 0) {
    at->state = AT_STATE_GET_CMD_STRING;
    return 0;
  } else if (isalpha(*buf->data)) {
    at->state = AT_STATE_GET_RESULT_STRING;
    return 0;
  }

  return -ENODATA;
}

static int at_state_get_cmd_string(struct at_client *at, struct net_buf *buf) { return get_response_string(at, buf, ':', AT_STATE_PROCESS_CMD); }

static bool is_cmer(struct at_client *at) {
  if (strncmp(at->buf, "CME ERROR", 9) == 0) {
    return true;
  }

  return false;
}

static int at_state_process_cmd(struct at_client *at, struct net_buf *buf) {
  if (is_cmer(at)) {
    at->state = AT_STATE_PROCESS_AG_NW_ERR;
    return 0;
  }

  if (at->resp) {
    at->resp(at, buf);
    at->resp = NULL;
    return 0;
  }
  at->state = AT_STATE_UNSOLICITED_CMD;
  return 0;
}

static int at_state_get_result_string(struct at_client *at, struct net_buf *buf) { return get_response_string(at, buf, '\r', AT_STATE_PROCESS_RESULT); }

static bool is_ring(struct at_client *at) {
  if (strncmp(at->buf, "RING", 4) == 0) {
    return true;
  }

  return false;
}

static int at_state_process_result(struct at_client *at, struct net_buf *buf) {
  enum at_cme    cme_err;
  enum at_result result;

  if (is_ring(at)) {
    at->state = AT_STATE_UNSOLICITED_CMD;
    return 0;
  }

  if (at_parse_result(at->buf, buf, &result) == 0) {
    if (at->finish) {
      /* cme_err is 0 - Is invalid until result is
       * AT_RESULT_CME_ERROR
       */
      cme_err = 0;
      at->finish(at, result, cme_err);
    }
  }

  /* Reset the state to process unsolicited response */
  at->cmd_state = AT_CMD_START;
  at->state     = AT_STATE_START;

  return 0;
}

int cme_handle(struct at_client *at) {
  enum at_cme cme_err;
  uint32_t    val;

  if (!at_get_number(at, &val) && val <= CME_ERROR_NETWORK_NOT_ALLOWED) {
    cme_err = val;
  } else {
    cme_err = CME_ERROR_UNKNOWN;
  }

  if (at->finish) {
    at->finish(at, AT_RESULT_CME_ERROR, cme_err);
  }

  return 0;
}

static int at_state_process_ag_nw_err(struct at_client *at, struct net_buf *buf) {
  at->cmd_state = AT_CMD_GET_VALUE;
  return at_parse_cmd_input(at, buf, NULL, cme_handle, AT_CMD_TYPE_NORMAL);
}

static int at_state_unsolicited_cmd(struct at_client *at, struct net_buf *buf) {
  if (at->unsolicited) {
    return at->unsolicited(at, buf);
  }

  return -ENODATA;
}

/* The order of handler function should match the enum at_state */
static handle_parse_input_t parser_cb[] = {
    at_state_start,             /* AT_STATE_START */
    at_state_start_cr,          /* AT_STATE_START_CR */
    at_state_start_lf,          /* AT_STATE_START_LF */
    at_state_get_cmd_string,    /* AT_STATE_GET_CMD_STRING */
    at_state_process_cmd,       /* AT_STATE_PROCESS_CMD */
    at_state_get_result_string, /* AT_STATE_GET_RESULT_STRING */
    at_state_process_result,    /* AT_STATE_PROCESS_RESULT */
    at_state_process_ag_nw_err, /* AT_STATE_PROCESS_AG_NW_ERR */
    at_state_unsolicited_cmd    /* AT_STATE_UNSOLICITED_CMD */
};

int at_parse_input(struct at_client *at, struct net_buf *buf) {
  int ret;

  while (buf->len) {
    if (at->state < AT_STATE_START || at->state >= AT_STATE_END) {
      return -EINVAL;
    }
    ret = parser_cb[at->state](at, buf);
    if (ret < 0) {
      /* Reset the state in case of error */
      at->cmd_state = AT_CMD_START;
      at->state     = AT_STATE_START;
      return ret;
    }
  }

  return 0;
}

static int at_cmd_start(struct at_client *at, struct net_buf *buf, const char *prefix, parse_val_t func, enum at_cmd_type type) {
  if (!str_has_prefix(at->buf, prefix)) {
    if (type == AT_CMD_TYPE_NORMAL) {
      at->state = AT_STATE_UNSOLICITED_CMD;
    }
    return -ENODATA;
  }

  if (type == AT_CMD_TYPE_OTHER) {
    /* Skip for Other type such as ..RING.. which does not have
     * values to get processed.
     */
    at->cmd_state = AT_CMD_PROCESS_VALUE;
  } else {
    at->cmd_state = AT_CMD_GET_VALUE;
  }

  return 0;
}

static int at_cmd_get_value(struct at_client *at, struct net_buf *buf, const char *prefix, parse_val_t func, enum at_cmd_type type) {
  /* Reset buffer before getting the values */
  reset_buffer(at);
  return get_cmd_value(at, buf, '\r', AT_CMD_PROCESS_VALUE);
}

static int at_cmd_process_value(struct at_client *at, struct net_buf *buf, const char *prefix, parse_val_t func, enum at_cmd_type type) {
  int ret;

  ret           = func(at);
  at->cmd_state = AT_CMD_STATE_END_LF;

  return ret;
}

static int at_cmd_state_end_lf(struct at_client *at, struct net_buf *buf, const char *prefix, parse_val_t func, enum at_cmd_type type) {
  int err;

  err = at_check_byte(buf, '\n');
  if (err < 0) {
    return err;
  }

  at->cmd_state = AT_CMD_START;
  at->state     = AT_STATE_START;
  return 0;
}

/* The order of handler function should match the enum at_cmd_state */
static handle_cmd_input_t cmd_parser_cb[] = {
    at_cmd_start,         /* AT_CMD_START */
    at_cmd_get_value,     /* AT_CMD_GET_VALUE */
    at_cmd_process_value, /* AT_CMD_PROCESS_VALUE */
    at_cmd_state_end_lf   /* AT_CMD_STATE_END_LF */
};

int at_parse_cmd_input(struct at_client *at, struct net_buf *buf, const char *prefix, parse_val_t func, enum at_cmd_type type) {
  int ret;

  while (buf->len) {
    if (at->cmd_state < AT_CMD_START || at->cmd_state >= AT_CMD_STATE_END) {
      return -EINVAL;
    }
    ret = cmd_parser_cb[at->cmd_state](at, buf, prefix, func, type);
    if (ret < 0) {
      return ret;
    }
    /* Check for main state, the end of cmd parsing and return. */
    if (at->state == AT_STATE_START) {
      return 0;
    }
  }

  return 0;
}

int at_has_next_list(struct at_client *at) { return at->buf[at->pos] != '\0'; }

int at_open_list(struct at_client *at) {
  skip_space(at);

  /* The list shall start with '(' open parenthesis */
  if (at->buf[at->pos] != '(') {
    return -ENODATA;
  }
  at->pos++;

  return 0;
}

int at_close_list(struct at_client *at) {
  skip_space(at);

  if (at->buf[at->pos] != ')') {
    return -ENODATA;
  }
  at->pos++;

  next_list(at);

  return 0;
}

int at_list_get_string(struct at_client *at, char *name, uint8_t len) {
  int i = 0;

  skip_space(at);

  if (at->buf[at->pos] != '"') {
    return -ENODATA;
  }
  at->pos++;

  while (at->buf[at->pos] != '\0' && at->buf[at->pos] != '"') {
    if (i == len) {
      return -ENODATA;
    }
    name[i++] = at->buf[at->pos++];
  }

  if (i == len) {
    return -ENODATA;
  }

  name[i] = '\0';

  if (at->buf[at->pos] != '"') {
    return -ENODATA;
  }
  at->pos++;

  skip_space(at);
  next_list(at);

  return 0;
}

int at_list_get_range(struct at_client *at, uint32_t *min, uint32_t *max) {
  uint32_t low, high;
  int      ret;

  ret = at_get_number(at, &low);
  if (ret < 0) {
    return ret;
  }

  if (at->buf[at->pos] == '-') {
    at->pos++;
    goto out;
  }

  if (!isdigit((unsigned char)at->buf[at->pos])) {
    return -ENODATA;
  }
out:
  ret = at_get_number(at, &high);
  if (ret < 0) {
    return ret;
  }

  *min = low;
  *max = high;

  next_list(at);

  return 0;
}

void at_register_unsolicited(struct at_client *at, at_resp_cb_t unsolicited) { at->unsolicited = unsolicited; }

void at_register(struct at_client *at, at_resp_cb_t resp, at_finish_cb_t finish) {
  at->resp   = resp;
  at->finish = finish;
  at->state  = AT_STATE_START;
}
